今日目标
- 线程状态
- 等待与唤醒
- Lambda表达式
- Stream流
第一章JDK8新特性
JDK新特性:
Lambda 表达式
默认方法【已学习过】
Stream API
方法引用
Base64
1.1 方法引用
5.1.1 方法引用概述
方法引用使得开发者可以直接引用现存的方法、Java类的构造方法或者实例对象。方法引用和Lambda表达式配合使用,使得java类的构造方法看起来紧凑而简洁,没有很多复杂的模板代码。
5.1.2 方法引用基本使用
方法引用使用一对冒号 :: 。
下面,我们在 Car 类中定义了 4 个方法作为例子来区分 Java 中 4 种不同方法的引用。
1 | public static class Car { |
第一种方法引用的类型是构造器引用,语法是Class::new,或者更一般的形式:Class
1 | final Car car = Car.create( Car::new ); |
第二种方法引用的类型是静态方法引用,语法是Class::static_method。注意:这个方法接受一个Car类型的参数。
1 | cars.forEach( Car::collide ); |
第三种方法引用的类型是某个类的成员方法的引用,语法是Class::method,注意,这个方法没有定义入参:
1 | cars.forEach( Car::repair ); |
第四种方法引用的类型是某个实例对象的成员方法的引用,语法是instance::method。注意:这个方法接受一个Car类型的参数:
1 | final Car police = Car.create( Car::new ); |
5.1.3 基于静态方法引用的代码演示
1 | public static void main(String args[]) { |
上面的代码,我们将 System.out::println 方法作为静态方法来引用。
测试结果为:
1 | 大明 |
第二章 Lambda表达式,研究
2.1 函数式编程思想概述
在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。
做什么,而不是怎么做
我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将run
方法体内的代码传递给Thread
类知晓。
传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。
2.2 Lambda的优化
当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable
接口来定义任务内容,并使用java.lang.Thread
类来启动该线程。
传统写法,代码如下:
1 | public class Demo01ThreadNameless { |
本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable
接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。
代码分析:
对于Runnable
的匿名内部类用法,可以分析出几点内容:
Thread
类需要Runnable
接口作为参数,其中的抽象run
方法是用来指定线程任务内容的核心;- 为了指定
run
的方法体,不得不需要Runnable
接口的实现类; - 为了省去定义一个
RunnableImpl
实现类的麻烦,不得不使用匿名内部类; - 必须覆盖重写抽象
run
方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错; - 而实际上,似乎只有方法体才是关键所在。
Lambda表达式写法,代码如下:
借助Java 8的全新语法,上述Runnable
接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:
1 | public class Demo02LambdaRunnable { |
这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!
2.3 Lambda的格式
标准格式:
Lambda省去面向对象的条条框框,格式由3个部分组成:
- 一些参数
- 一个箭头
- 一段代码
Lambda表达式的标准格式为:
1 | (参数类型 参数名称) -> { 代码语句 } |
格式说明:
- 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
->
是新引入的语法格式,代表指向动作。- 大括号内的语法与传统方法体要求基本一致。
匿名内部类与lambda对比:
1 | new Thread(new Runnable() { |
仔细分析该代码中,Runnable
接口只有一个run
方法的定义:
public abstract void run();
即制定了一种做事情的方案(其实就是一个方法):
- 无参数:不需要任何条件即可执行该方案。
- 无返回值:该方案不产生任何结果。
- 代码块(方法体):该方案的具体执行步骤。
同样的语义体现在Lambda
语法中,要更加简单:
1 | () -> System.out.println("多线程任务执行!") |
- 前面的一对小括号即
run
方法的参数(无),代表不需要任何条件; - 中间的一个箭头代表将前面的参数传递给后面的代码;
- 后面的输出语句即业务逻辑代码。
参数和返回值:
下面举例演示java.util.Comparator<T>
接口的使用场景代码,其中的抽象方法定义为:
public abstract int compare(T o1, T o2);
当需要对一个对象数组进行排序时,Arrays.sort
方法需要一个Comparator
接口实例来指定排序的规则。假设有一个Person
类,含有String name
和int age
两个成员变量:
1 | public class Person { |
传统写法
如果使用传统的代码对Person[]
数组进行排序,写法如下:
1 | public class Demo06Comparator { |
这种做法在面向对象的思想中,似乎也是“理所当然”的。其中Comparator
接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。
代码分析
下面我们来搞清楚上述代码真正要做什么事情。
- 为了排序,
Arrays.sort
方法需要排序规则,即Comparator
接口的实例,抽象方法compare
是关键; - 为了指定
compare
的方法体,不得不需要Comparator
接口的实现类; - 为了省去定义一个
ComparatorImpl
实现类的麻烦,不得不使用匿名内部类; - 必须覆盖重写抽象
compare
方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错; - 实际上,只有参数和方法体才是关键。
Lambda写法
1 | public class Demo07ComparatorLambda { |
省略格式:
省略规则
在Lambda标准格式的基础上,使用省略写法的规则为:
- 小括号内参数的类型可以省略;
- 如果小括号内有且仅有一个参,则小括号可以省略;
- 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。
备注:掌握这些省略规则后,请对应地回顾本章开头的多线程案例。
可推导即可省略
Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以根据上下文推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:
1 | Runnable接口简化: |
2.4 Lambda的前提条件
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
无论是JDK内置的Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。 - 使用Lambda必须具有上下文推断。
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
备注:有且仅有一个抽象方法的接口,称为“函数式接口”。
2.5 Lambda表达式和匿名内部类的区别
所需类型不同 匿名内部类:可以是接口,也可以是抽象类,还可以是具体类。 Lambda表达式:只能是接口
使用限制不同 如果接口中有且仅有一个抽象方法,可以使用Lambda表达式,也可以使用匿名内部类。 如果接口中多于一个抽象方法,那么只能使用匿名内部类,而不能使用Lambda表达式。
实现原理不同
匿名内部类:编译之后会产生一个单独的.class字节码文件
Lambda表达式:编译之后不会产生一个单独的.class字节码文件。对应的字节码会在运行的时候动态生成。
第三章 Stream
在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。
3.1 引言
传统集合的多步遍历代码
几乎所有的集合(如Collection
接口或Map
接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:
1 | public class Demo01ForEach { |
这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。
循环遍历的弊端
Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How),这点此前已经结合内部类进行了对比说明。现在,我们仔细体会一下上例代码,可以发现:
- for循环的语法就是“怎么做”
- for循环的循环体才是“做什么”
为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。
试想一下,如果希望对集合中的元素进行筛选过滤:
- 将集合A根据条件一过滤为子集B;
- 然后再根据条件二过滤为子集C。
那怎么办?在Java 8之前的做法可能为:
1 | public class Demo02NormalFilter { |
这段代码中含有三个循环,每一个作用不同:
- 首先筛选所有姓张的人;
- 然后筛选名字有三个字的人;
- 最后进行对结果进行打印输出。
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。
那,Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢?
Stream的更优写法
下面来看一下借助Java 8的Stream API,什么才叫优雅:
1 | public class Demo03StreamFilter { |
直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。
3.2 流式思想概述
注意:请暂时忘记对传统IO流的固有印象!
整体来看,流式思想类似于工厂车间的“生产流水线”。
当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤方案,然后再按照方案去执行它。
这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字3是最终结果。
这里的filter
、map
、skip
都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法count
执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。
备注:“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。
3.3 获取流方式
java.util.stream.Stream<T>
是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)
获取一个流非常简单,有以下几种常用的方式:
- 所有的
Collection
集合都可以通过stream
默认方法获取流; Stream
接口的静态方法of
可以获取数组对应的流。
方式1 : 根据Collection获取流
首先,java.util.Collection
接口中加入了default方法stream
用来获取流,所以其所有实现类均可获取流。
1 | import java.util.*; |
方式2 : 根据Map获取流
java.util.Map
接口不是Collection
的子接口,且其K-V数据结构不符合流元素的单一特征,所以获取对应的流需要分key、value或entry等情况:
1 | import java.util.HashMap; |
方式3 : 根据数组获取流
如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以Stream
接口中提供了静态方法of
,使用很简单:
1 | import java.util.stream.Stream; |
备注:
of
方法的参数其实是一个可变参数,所以支持数组。
3.4 常用方法
流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:
- 终结方法:返回值类型不再是
Stream
接口自身类型的方法,因此不再支持类似StringBuilder
那样的链式调用。本小节中,终结方法包括count
和forEach
方法。 - 非终结方法:返回值类型仍然是
Stream
接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)
函数拼接与终结方法
在上述介绍的各种方法中,凡是返回值仍然为Stream
接口的为函数拼接方法,它们支持链式调用;而返回值不再为Stream
接口的为终结方法,不再支持链式调用。如下表所示:
方法名 | 方法作用 | 方法种类 | 是否支持链式调用 |
---|---|---|---|
count | 统计个数 | 终结 | 否 |
forEach | 逐一处理 | 终结 | 否 |
filter | 过滤 | 函数拼接 | 是 |
limit | 取用前几个 | 函数拼接 | 是 |
skip | 跳过前几个 | 函数拼接 | 是 |
map | 映射 | 函数拼接 | 是 |
concat | 组合 | 函数拼接 | 是 |
备注:本小节之外的更多方法,请自行参考API文档。
forEach : 逐一处理
虽然方法名字叫forEach
,但是与for循环中的“for-each”昵称不同,该方法并不保证元素的逐一消费动作在流中是被有序执行的。
1 | void forEach(Consumer<? super T> action); |
该方法接收一个Consumer
接口函数,会将每一个流元素交给该函数进行处理。例如:
1 | import java.util.stream.Stream; |
count:统计个数
正如旧集合Collection
当中的size
方法一样,流提供count
方法来数一数其中的元素个数:
1 | long count(); |
该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。基本使用:
1 | public class Demo09StreamCount { |
filter:过滤
可以通过filter
方法将一个流转换成另一个子集流。方法声明:
1 | Stream<T> filter(Predicate<? super T> predicate); |
该接口接收一个Predicate
函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。
基本使用
Stream流中的filter
方法基本使用的代码如:
1 | public class Demo07StreamFilter { |
在这里通过Lambda表达式来指定了筛选的条件:必须姓张。
limit:取用前几个
limit
方法可以对流进行截取,只取用前n个。方法签名:
1 | Stream<T> limit(long maxSize); |
参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。基本使用:
1 | import java.util.stream.Stream; |
skip:跳过前几个
如果希望跳过前几个元素,可以使用skip
方法获取一个截取之后的新流:
1 | Stream<T> skip(long n); |
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:
1 | import java.util.stream.Stream; |
map:映射
如果需要将流中的元素映射到另一个流中,可以使用map
方法。方法签名:
1 | <R> Stream<R> map(Function<? super T, ? extends R> mapper); |
该接口需要一个Function
函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
基本使用
Stream流中的map
方法基本使用的代码如:
1 | import java.util.stream.Stream; |
这段代码中,map
方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为Integer
类对象)。
concat:组合
如果有两个流,希望合并成为一个流,那么可以使用Stream
接口的静态方法concat
:
1 | static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) |
备注:这是一个静态方法,与
java.lang.String
当中的concat
方法是不同的。
该方法的基本使用代码如:
1 | import java.util.stream.Stream; |
3.5 Stream综合案例
现在有两个ArrayList
集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下若干操作步骤:
- 第一个队伍只要名字为3个字的成员姓名;
- 第一个队伍筛选之后只要前3个人;
- 第二个队伍只要姓张的成员姓名;
- 第二个队伍筛选之后不要前2个人;
- 将两个队伍合并为一个队伍;
- 根据姓名创建
Person
对象; - 打印整个队伍的Person对象信息。
两个队伍(集合)的代码如下:
1 | public class DemoArrayListNames { |
而Person
类的代码为:
1 | public class Person { |
传统方式
使用for循环 , 示例代码:
1 | public class DemoArrayListNames { |
运行结果为:
1 | Person{name='宋远桥'} |
Stream方式
等效的Stream流式处理代码为:
1 | public class DemoStreamNames { |
运行效果完全一样:
1 | Person{name='宋远桥'} |
3.6 收集Stream结果
对流操作完成之后,如果需要将其结果进行收集,例如获取对应的集合、数组等,如何操作?
收集到集合中
Stream流提供collect
方法,其参数需要一个java.util.stream.Collector<T,A, R>
接口对象来指定收集到哪种集合中。幸运的是,java.util.stream.Collectors
类提供一些方法,可以作为Collector
接口的实例:
public static <T> Collector<T, ?, List<T>> toList()
:转换为List
集合。public static <T> Collector<T, ?, Set<T>> toSet()
:转换为Set
集合。
下面是这两个方法的基本使用代码:
1 | import java.util.List; |
收集到数组中
Stream提供toArray
方法来将结果放到一个数组中,由于泛型擦除的原因,返回值类型是Object[]的:
1 | Object[] toArray(); |
其使用场景如:
1 | import java.util.stream.Stream; |
第四章 File类
4.1 概述
java.io.File
类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。
4.2 构造方法
public File(String pathname)
:通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。public File(String parent, String child)
:从父路径名字符串和子路径名字符串创建新的 File实例。public File(File parent, String child)
:从父抽象路径名和子路径名字符串创建新的 File实例。构造举例,代码如下:
1 | // 文件路径名 |
小贴士:
- 一个File对象代表硬盘中实际存在的一个文件或者目录。
- 无论该路径下是否存在文件或者目录,都不影响File对象的创建。
4.3 常用方法
获取功能的方法
public String getAbsolutePath()
:返回此File的绝对路径名字符串。public String getPath()
:将此File转换为路径名字符串。public String getName()
:返回由此File表示的文件或目录的名称。public long length()
:返回由此File表示的文件的长度。方法演示,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class FileGet {
public static void main(String[] args) {
File f = new File("d:/aaa/bbb.java");
System.out.println("文件绝对路径:"+f.getAbsolutePath());
System.out.println("文件构造路径:"+f.getPath());
System.out.println("文件名称:"+f.getName());
System.out.println("文件长度:"+f.length()+"字节");
File f2 = new File("d:/aaa");
System.out.println("目录绝对路径:"+f2.getAbsolutePath());
System.out.println("目录构造路径:"+f2.getPath());
System.out.println("目录名称:"+f2.getName());
System.out.println("目录长度:"+f2.length());
}
}
输出结果:
文件绝对路径:d:\aaa\bbb.java
文件构造路径:d:\aaa\bbb.java
文件名称:bbb.java
文件长度:636字节
目录绝对路径:d:\aaa
目录构造路径:d:\aaa
目录名称:aaa
目录长度:4096
API中说明:length(),表示文件的长度。但是File对象表示目录,则返回值未指定。
绝对路径和相对路径
- 绝对路径:从盘符开始的路径,这是一个完整的路径。
- 相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。
1 | public class FilePath { |
判断功能的方法
public boolean exists()
:此File表示的文件或目录是否实际存在。public boolean isDirectory()
:此File表示的是否为目录。public boolean isFile()
:此File表示的是否为文件。
方法演示,代码如下:
1 | public class FileIs { |
创建删除功能的方法
public boolean createNewFile()
:当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。public boolean delete()
:删除由此File表示的文件或目录。public boolean mkdir()
:创建由此File表示的目录。public boolean mkdirs()
:创建由此File表示的目录,包括任何必需但不存在的父目录。
方法演示,代码如下:
1 | public class FileCreateDelete { |
API中说明:delete方法,如果此File表示目录,则目录必须为空才能删除。
4.4 目录的遍历
public String[] list()
:返回一个String数组,表示该File目录中的所有子文件或目录。public File[] listFiles()
:返回一个File数组,表示该File目录中的所有的子文件或目录。
1 | public class FileFor { |
小贴士:
调用listFiles方法的File对象,表示的必须是实际存在的目录,否则返回null,无法进行遍历。
第五章 递归
5.1 概述
- 递归:指在当前方法内调用自己的这种现象。
1 | public static void a(){ |
5.2 递归累和
计算1 ~ n的和
分析:num的累和 = num + (num-1)的累和,所以可以把累和的操作定义成一个方法,递归调用。
实现代码:
1 | public class DiGuiDemo { |
小贴士:递归一定要有条件限定,保证递归能够停止下来,次数不要太多,否则会发生栈内存溢出。
5.3 递归求阶乘
- 阶乘:所有小于及等于该数的正整数的积。
1 | n的阶乘:n! = n * (n-1) *...* 3 * 2 * 1 |
分析:这与累和类似,只不过换成了乘法运算,学员可以自己练习,需要注意阶乘值符合int类型的范围。
1 | 推理得出:n! = n * (n-1)! |
代码实现:
1 | public class DiGuiDemo { |
5.4 文件搜索
搜索D:\aaa
目录中的.java
文件。
分析:
- 目录搜索,无法判断多少级目录,所以使用递归,遍历所有目录。
- 遍历目录时,获取的子文件,通过文件名称,判断是否符合条件。
代码实现:
1 | public class DiGuiDemo3 { |
第六章 IO概述
6.1 什么是IO
生活中,你肯定经历过这样的场景。当你编辑一个文本文件,忘记了ctrl+s
,可能文件就白白编辑了。当你电脑上插入一个U盘,可以把一个视频,拷贝到你的电脑硬盘里。那么数据都是在哪些设备上的呢?键盘、内存、硬盘、外接设备等等。
我们把这种数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为输入input
和输出output
,即流向内存是输入流,流出内存的输出流。
Java中I/O操作主要是指使用java.io
包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据。
6.2 IO的分类
根据数据的流向分为:输入流和输出流。
- 输入流 :把数据从
其他设备
上读取到内存
中的流。 - 输出流 :把数据从
内存
中写出到其他设备
上的流。
格局数据的类型分为:字节流和字符流。
- 字节流 :以字节为单位,读写数据的流。
- 字符流 :以字符为单位,读写数据的流。
6.4 顶级父类们
输入流 | 输出流 | |
---|---|---|
字节流 | 字节输入流 InputStream |
字节输出流 OutputStream |
字符流 | 字符输入流 Reader |
字符输出流 Writer |
第七章 字节流
7.1 一切皆为字节
一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。
7.2 字节输出流【OutputStream】
java.io.OutputStream
抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。
public void close()
:关闭此输出流并释放与此流相关联的任何系统资源。public void flush()
:刷新此输出流并强制任何缓冲的输出字节被写出。public void write(byte[] b)
:将 b.length字节从指定的字节数组写入此输出流。public void write(byte[] b, int off, int len)
:从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。public abstract void write(int b)
:将指定的字节输出流。
小贴士:
close方法,当完成流的操作时,必须调用此方法,释放系统资源。
7.3 FileOutputStream类
OutputStream
有很多子类,我们从最简单的一个子类开始。
java.io.FileOutputStream
类是文件输出流,用于将数据写出到文件。
构造方法
public FileOutputStream(File file)
:创建文件输出流以写入由指定的 File对象表示的文件。public FileOutputStream(String name)
: 创建文件输出流以指定的名称写入文件。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。
- 构造举例,代码如下:
1 | public class FileOutputStreamConstructor throws IOException { |
写出字节数据
- 写出字节:
write(int b)
方法,每次可以写出一个字节数据,代码使用演示:
1 | public class FOSWrite { |
小贴士:
- 虽然参数为int类型四个字节,但是只会保留一个字节的信息写出。
- 流操作完毕后,必须释放系统资源,调用close方法,千万记得。
- 写出字节数组:
write(byte[] b)
,每次可以写出数组中的数据,代码使用演示:
1 | public class FOSWrite { |
- 写出指定长度字节数组:
write(byte[] b, int off, int len)
,每次写出从off索引开始,len个字节,代码使用演示:
1 | public class FOSWrite { |
数据追加续写
经过以上的演示,每次程序运行,创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续添加新数据呢?
public FileOutputStream(File file, boolean append)
: 创建文件输出流以写入由指定的 File对象表示的文件。public FileOutputStream(String name, boolean append)
: 创建文件输出流以指定的名称写入文件。
这两个构造方法,参数中都需要传入一个boolean类型的值,true
表示追加数据,false
表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了,代码使用演示:
1 | public class FOSWrite { |
写出换行
Windows系统里,换行符号是\r\n
。把
以指定是否追加续写了,代码使用演示:
1 | public class FOSWrite { |
- 回车符
\r
和换行符\n
:
- 回车符:回到一行的开头(return)。
- 换行符:下一行(newline)。
- 系统中的换行:
- Windows系统里,每行结尾是
回车+换行
,即\r\n
;- Unix系统里,每行结尾只有
换行
,即\n
;- Mac系统里,每行结尾是
回车
,即\r
。从 Mac OS X开始与Linux统一。
7.4 字节输入流【InputStream】
java.io.InputStream
抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
public void close()
:关闭此输入流并释放与此流相关联的任何系统资源。public abstract int read()
: 从输入流读取数据的下一个字节。public int read(byte[] b)
: 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。
小贴士:
close方法,当完成流的操作时,必须调用此方法,释放系统资源。
7.5 FileInputStream类
java.io.FileInputStream
类是文件输入流,从文件中读取字节。
构造方法
FileInputStream(File file)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。FileInputStream(String name)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出FileNotFoundException
- 构造举例,代码如下:
1 | public class FileInputStreamConstructor throws IOException{ |
读取字节数据
- 读取字节:
read
方法,每次可以读取一个字节的数据,提升为int类型,读取到文件末尾,返回-1
,代码使用演示:
1 | public class FISRead { |
循环改进读取方式,代码使用演示:
1 | public class FISRead { |
小贴士:
- 虽然读取了一个字节,但是会自动提升为int类型。
- 流操作完毕后,必须释放系统资源,调用close方法,千万记得。
- 使用字节数组读取:
read(byte[] b)
,每次读取b的长度个字节到数组中,返回读取到的有效字节个数,读取到末尾时,返回-1
,代码使用演示:
1 | public class FISRead { |
错误数据d
,是由于最后一次读取时,只读取一个字节e
,数组中,上次读取的数据没有被完全替换,所以要通过len
,获取有效的字节,代码使用演示:
1 | public class FISRead { |
小贴士:
使用数组读取,每次读取多个字节,减少了系统间的IO操作次数,从而提高了读写的效率,建议开发中使用。
4.6 字节流练习:图片复制
案例实现
复制图片文件,代码使用演示:
1 | public class Copy { |
小贴士:
流的关闭原则:先开后关,后开先关。